package com.ihr360.job.core;

import org.springframework.util.StringUtils;

import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;

public class ExitStatus implements Serializable, Comparable<ExitStatus> {
    /**
     * Convenient constant value representing unknown state - assumed not
     * continuable.
     */
    public static final ExitStatus UNKNOWN = new ExitStatus("UNKNOWN");

    /**
     * Convenient constant value representing continuable state where processing
     * is still taking place, so no further action is required. Used for
     * asynchronous execution scenarios where the processing is happening in
     * another thread or process and the caller is not required to wait for the
     * result.
     */
    public static final ExitStatus EXECUTING = new ExitStatus("EXECUTING");

    /**
     * Convenient constant value representing finished processing.
     */
    public static final ExitStatus COMPLETED = new ExitStatus("COMPLETED");

    /**
     * Convenient constant value representing job that did no processing (e.g.
     * because it was already complete).
     */
    public static final ExitStatus NOOP = new ExitStatus("NOOP");

    /**
     * Convenient constant value representing finished processing with an error.
     */
    public static final ExitStatus FAILED = new ExitStatus("FAILED");

    /**
     * Convenient constant value representing finished processing with
     * interrupted status.
     */
    public static final ExitStatus STOPPED = new ExitStatus("STOPPED");

    private final String exitCode;

    private final String exitDescription;

    private final String exitMessage;

    public ExitStatus(String exitCode) {
        this(exitCode, "");
    }

    public ExitStatus(String exitCode, String exitDescription) {
        this(exitCode, "", "");
    }

    public ExitStatus(String exitCode, String exitDescription, String exitMessage) {
        super();
        this.exitCode = exitCode;
        this.exitDescription = exitDescription == null ? "" : exitDescription;
        this.exitMessage = exitMessage;
    }

    /**
     * Getter for the exit code (defaults to blank).
     *
     * @return the exit code.
     */
    public String getExitCode() {
        return exitCode;
    }

    /**
     * Getter for the exit description (defaults to blank)
     */
    public String getExitDescription() {
        return exitDescription;
    }

    public String getExitMessage() {
        return exitMessage;
    }

    /**
     * Create a new {@link ExitStatus} with a logical combination of the exit
     * code, and a concatenation of the descriptions. If either value has a
     * higher severity then its exit code will be used in the result. In the
     * case of equal severity, the exit code is replaced if the new value is
     * alphabetically greater.<br>
     * <br>
     * <p>
     * Severity is defined by the exit code:
     * <ul>
     * <li>Codes beginning with EXECUTING have severity 1</li>
     * <li>Codes beginning with COMPLETED have severity 2</li>
     * <li>Codes beginning with NOOP have severity 3</li>
     * <li>Codes beginning with STOPPED have severity 4</li>
     * <li>Codes beginning with FAILED have severity 5</li>
     * <li>Codes beginning with UNKNOWN have severity 6</li>
     * </ul>
     * Others have severity 7, so custom exit codes always win.<br>
     * <p>
     * If the input is null just return this.
     *
     * @param status an {@link ExitStatus} to combine with this one.
     * @return a new {@link ExitStatus} combining the current value and the
     * argument provided.
     */
    public ExitStatus and(ExitStatus status) {
        if (status == null) {
            return this;
        }
        ExitStatus result = addExitDescription(status.exitDescription,status.getExitMessage());
        if (compareTo(status) < 0) {
            result = result.replaceExitCode(status.exitCode);
        }
        return result;
    }

    /**
     * @param status an {@link ExitStatus} to compare
     * @return greater than zero, 0, less than zero according to the severity and exit code
     * @see java.lang.Comparable
     */
    @Override
    public int compareTo(ExitStatus status) {
        if (severity(status) > severity(this)) {
            return -1;
        }
        if (severity(status) < severity(this)) {
            return 1;
        }
        return this.getExitCode().compareTo(status.getExitCode());
    }

    /**
     * @param status
     * @return
     */
    private int severity(ExitStatus status) {
        if (status.exitCode.startsWith(EXECUTING.exitCode)) {
            return 1;
        }
        if (status.exitCode.startsWith(COMPLETED.exitCode)) {
            return 2;
        }
        if (status.exitCode.startsWith(NOOP.exitCode)) {
            return 3;
        }
        if (status.exitCode.startsWith(STOPPED.exitCode)) {
            return 4;
        }
        if (status.exitCode.startsWith(FAILED.exitCode)) {
            return 5;
        }
        if (status.exitCode.startsWith(UNKNOWN.exitCode)) {
            return 6;
        }
        return 7;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return String.format("exitCode=%s;exitDescription=%s", exitCode, exitDescription);
    }

    /**
     * Compare the fields one by one.
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        return toString().equals(obj.toString());
    }

    /**
     * Compatible with the equals implementation.
     *
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    /**
     * Add an exit code to an existing {@link ExitStatus}. If there is already a
     * code present tit will be replaced.
     *
     * @param code the code to add
     * @return a new {@link ExitStatus} with the same properties but a new exit
     * code.
     */
    public ExitStatus replaceExitCode(String code) {
        return new ExitStatus(code, exitDescription);
    }

    /**
     * Check if this status represents a running process.
     *
     * @return true if the exit code is "EXECUTING" or "UNKNOWN"
     */
    public boolean isRunning() {
        return "EXECUTING".equals(this.exitCode) || "UNKNOWN".equals(this.exitCode);
    }

    /**
     * Add an exit description to an existing {@link ExitStatus}. If there is
     * already a description present the two will be concatenated with a
     * semicolon.
     *
     * @param description the description to add
     * @return a new {@link ExitStatus} with the same properties but a new exit
     * description
     */
    public ExitStatus addExitDescription(String description,String message) {
        StringBuilder buffer = new StringBuilder();
        boolean changed = StringUtils.hasText(description) && !exitDescription.equals(description);
        if (StringUtils.hasText(exitDescription)) {
            buffer.append(exitDescription);
            if (changed) {
                buffer.append("; ");
            }
        }
        if (changed) {
            buffer.append(description);
        }
        return new ExitStatus(exitCode, buffer.toString(),message);
    }

    /**
     * Extract the stack trace from the throwable provided and append it to
     * the exist description.
     *
     * @param throwable
     * @return a new ExitStatus with the stack trace appended
     */
    public ExitStatus addExitDescription(Throwable throwable) {
        StringWriter writer = new StringWriter();
        throwable.printStackTrace(new PrintWriter(writer));
        String desc = writer.toString();
        String message = throwable.getMessage();
        return addExitDescription(desc,message);
    }

    /**
     * @param status the exit code to be evaluated
     * @return true if the value matches a known exit code
     */
    public static boolean isNonDefaultExitStatus(ExitStatus status) {
        return status == null || status.getExitCode() == null ||
                status.getExitCode().equals(ExitStatus.COMPLETED.getExitCode()) ||
                status.getExitCode().equals(ExitStatus.EXECUTING.getExitCode()) ||
                status.getExitCode().equals(ExitStatus.FAILED.getExitCode()) ||
                status.getExitCode().equals(ExitStatus.NOOP.getExitCode()) ||
                status.getExitCode().equals(ExitStatus.STOPPED.getExitCode()) ||
                status.getExitCode().equals(ExitStatus.UNKNOWN.getExitCode());
    }
}